"use strict";
/*
*  Copyright (C) 1998-2019 by Northwoods Software Corporation. All Rights Reserved.
*/

// A custom Tool for drawing polygons or polylines

import * as go from "../release/go";

/**
* @constructor
* @extends Tool
* @class
* This tool allows the user to draw a new polygon or polyline shape by clicking where the corners should go.
* Right click or type ENTER to finish the operation.
* <p/>
* Set {@link #isPolygon} to false if you want this tool to draw open unfilled polyline shapes.
* Set {@link #archetypePartData} to customize the node data object that is added to the model.
* Data-bind to those properties in your node template to customize the appearance and behavior of the part.
* <p/>
* This tool uses a temporary {@link Shape}, {@link #temporaryShape}, held by a {@link Part} in the "Tool" layer,
* to show interactively what the user is drawing.
*/



export class PolygonDrawingTool extends go.Tool {
	private _isPolygon: boolean = true;
	private _hasArcs: boolean = false;
	private _archetypePartData: Object = {}; // the data to copy for a new polygon Part

	// this is the Shape that is shown during a drawing operation
	private _temporaryShape: go.Shape = go.GraphObject.make(go.Shape, { name: "SHAPE", fill: "lightgray", strokeWidth: 1.5 });
	// the Shape has to be inside a temporary Part that is used during the drawing operation
	private temp: go.Part = go.GraphObject.make(go.Part, { layerName: "Tool" }, this._temporaryShape);

	constructor() {
		super();
		this.name = "PolygonDrawing";
	}

  /**
  * Don't start this tool in a mode-less fashion when the user's mouse-down is on an existing Part.
  * When this tool is a mouse-down tool, it requires using the left mouse button in the background of a modifiable Diagram.
  * Modal uses of this tool will not call this canStart predicate.
  * @this {PolygonDrawingTool}
  */
  public canStart(): boolean {
		if (!this.isEnabled) return false;
		var diagram = this.diagram;
		if (diagram === null || diagram.isReadOnly || diagram.isModelReadOnly) return false;
		var model = diagram.model;
		if (model === null) return false;
		// require left button
		if (!diagram.firstInput.left) return false;
		// can't start when mouse-down on an existing Part
		var obj = diagram.findObjectAt(diagram.firstInput.documentPoint, null, null);
		return (obj === null);
	};

  /**
  * Start a transaction, capture the mouse, use a "crosshair" cursor,
  * and start accumulating points in the geometry of the {@link #temporaryShape}.
  * @this {PolygonDrawingTool}
  */
	public doActivate() {
		super.doActivate.call(this);
		var diagram = this.diagram;
		this.startTransaction(this.name);
		if (!diagram.lastInput.isTouchEvent) diagram.isMouseCaptured = true;
		diagram.currentCursor = "crosshair";
		// the first point
		if (!diagram.lastInput.isTouchEvent) this.addPoint(diagram.lastInput.documentPoint);
	};

  /**
  * Stop the transaction and clean up.
  * @this {PolygonDrawingTool}
  */
	public doDeactivate() {
		super.doDeactivate.call(this);
		var diagram = this.diagram;
		if (this.temporaryShape !== null) {
			diagram.remove(this.temporaryShape.part);
		}
		diagram.currentCursor = "";
		if (diagram.isMouseCaptured) diagram.isMouseCaptured = false;
		this.stopTransaction();
	};

  /**
  * This internal method adds a segment to the geometry of the {@link #temporaryShape}.
  * @this {PolygonDrawingTool}
  */
	public addPoint(p: go.Point) {
		var shape = this.temporaryShape;
		if (shape === null) return;

		// for the temporary Shape, normalize the geometry to be in the viewport
		var viewpt = this.diagram.viewportBounds.position;
		var q = new go.Point(p.x - viewpt.x, p.y - viewpt.y);

		var part = shape.part;
		// if it's not in the Diagram, re-initialize the Shape's geometry and add the Part to the Diagram
		if (part.diagram === null) {
			var fig = new go.PathFigure(q.x, q.y, true);  // possibly filled, depending on Shape.fill
			var geo: go.Geometry = new go.Geometry().add(fig);  // the Shape.geometry consists of a single PathFigure
			this.temporaryShape.geometry = geo;
			// position the Shape's Part, accounting for the stroke width
			part.position = viewpt.copy().offset(-shape.strokeWidth / 2, -shape.strokeWidth / 2);
			this.diagram.add(part);
		} else {
			// must copy whole Geometry in order to add a PathSegment
			var geo: go.Geometry = shape.geometry.copy();
			var fig = geo.figures.first();
			if (this.hasArcs) {
				var lastseg = fig.segments.last();
				if (lastseg === null) {
					fig.add(new go.PathSegment(go.PathSegment.QuadraticBezier, q.x, q.y, (fig.startX + q.x) / 2, (fig.startY + q.y) / 2));
				} else {
					fig.add(new go.PathSegment(go.PathSegment.QuadraticBezier, q.x, q.y, (lastseg.endX + q.x) / 2, (lastseg.endY + q.y) / 2));
				}
			} else {
				fig.add(new go.PathSegment(go.PathSegment.Line, q.x, q.y));
			}
		}
		shape.geometry = geo;
	};

  /**
  * This internal method changes the last segment of the geometry of the {@link #temporaryShape} to end at the given point.
  * @this {PolygonDrawingTool}
  */
	public moveLastPoint(p: go.Point) {
		// must copy whole Geometry in order to change a PathSegment
		var shape = this.temporaryShape;
		var geo = shape.geometry.copy();
		var fig = geo.figures.first();
		var segs = fig.segments;
		if (segs.count > 0) {
			// for the temporary Shape, normalize the geometry to be in the viewport
			var viewpt = this.diagram.viewportBounds.position;
			var seg = segs.elt(segs.count - 1);
			// modify the last PathSegment to be the given Point p
			seg.endX = p.x - viewpt.x;
			seg.endY = p.y - viewpt.y;
			if (seg.type === go.PathSegment.QuadraticBezier) {
				var prevx = 0.0;
				var prevy = 0.0;
				if (segs.count > 1) {
					var prevseg = segs.elt(segs.count - 2);
					prevx = prevseg.endX;
					prevy = prevseg.endY;
				} else {
					prevx = fig.startX;
					prevy = fig.startY;
				}
				seg.point1X = (seg.endX + prevx) / 2;
				seg.point1Y = (seg.endY + prevy) / 2;
			}
			shape.geometry = geo;
		}
	};

  /**
  * This internal method removes the last segment of the geometry of the {@link #temporaryShape}.
  * @this {PolygonDrawingTool}
  */
	public removeLastPoint() {
		// must copy whole Geometry in order to remove a PathSegment
		var shape = this.temporaryShape;
		var geo = shape.geometry.copy();
		var segs = geo.figures.first().segments;
		if (segs.count > 0) {
			segs.removeAt(segs.count - 1);
			shape.geometry = geo;
		}
	};

  /**
  * Add a new node data JavaScript object to the model and initialize the Part's
  * position and its Shape's geometry by copying the {@link #temporaryShape}'s {@link Shape#geometry}.
  * @this {PolygonDrawingTool}
  */
	public finishShape() {
		var diagram = this.diagram;
		var shape: go.Shape = this.temporaryShape;
		if (shape !== null && this.archetypePartData !== null) {
			// remove the temporary point, which is last, except on touch devices
			if (!diagram.lastInput.isTouchEvent) this.removeLastPoint();
			var tempgeo = shape.geometry;
			// require 3 points (2 segments) if polygon; 2 points (1 segment) if polyline
			if (tempgeo.figures.first().segments.count >= (this.isPolygon ? 2 : 1)) {
				// normalize geometry and node position
				var viewpt = diagram.viewportBounds.position;
				var geo = tempgeo.copy();
				if (this.isPolygon) {
					// if polygon, close the last segment
					var segs = geo.figures.first().segments;
					var seg = segs.elt(segs.count - 1);
					seg.isClosed = true;
				}
				// create the node data for the model
				var d = diagram.model.copyNodeData(this.archetypePartData);
				// adding data to model creates the actual Part
				diagram.model.addNodeData(d);
				var part = diagram.findPartForData(d);
				// assign the position for the whole Part
				var pos = geo.normalize();
				pos.x = viewpt.x - pos.x - shape.strokeWidth / 2;
				pos.y = viewpt.y - pos.y - shape.strokeWidth / 2;
				part.position = pos;
				// assign the Shape.geometry
				var shape: go.Shape = part.findObject("SHAPE") as go.Shape;
				if (shape !== null) shape.geometry = geo;
				this.transactionResult = this.name;
			}
		}
		this.stopTool();
	};

  /**
  * Add another point to the geometry of the {@link #temporaryShape}.
  * @this {PolygonDrawingTool}
  */
	public doMouseDown() {
		if (!this.isActive) {
			this.doActivate();
		}
		// a new temporary end point, the previous one is now "accepted"
		this.addPoint(this.diagram.lastInput.documentPoint);
		if (!this.diagram.lastInput.left) {  // e.g. right mouse down
			this.finishShape();
		} else if (this.diagram.lastInput.clickCount > 1) {  // e.g. double-click
			this.removeLastPoint();
			this.finishShape();
		}
	};

  /**
  * Move the last point of the {@link #temporaryShape}'s geometry to follow the mouse point.
  * @this {PolygonDrawingTool}
  */
	public doMouseMove() {
		if (this.isActive) {
			this.moveLastPoint(this.diagram.lastInput.documentPoint);
		}
	};

  /**
  * Do not stop this tool, but continue to accumulate Points via mouse-down events.
  * @this {PolygonDrawingTool}
  */
	public doMouseUp() {
		// don't stop this tool (the default behavior is to call stopTool)
	};

  /**
  * Typing the "ENTER" key accepts the current geometry (excluding the current mouse point)
  * and creates a new part in the model by calling {@link #finishShape}.
  * <p/>
  * Typing the "Z" key causes the previous point to be discarded.
  * <p/>
  * Typing the "ESCAPE" key causes the temporary Shape and its geometry to be discarded and this tool to be stopped.
  * @this {PolygonDrawingTool}
  */
	public doKeyDown() {
		if (!this.isActive) return;
		var e = this.diagram.lastInput;
		if (e.key === '\r') {  // accept
			this.finishShape();  // all done!
		} else if (e.key === 'Z') {  // undo
			this.undo();
		} else {
			super.doKeyDown.call(this);
		}
	};

  /**
  * Undo: remove the last point and continue the drawing of new points.
  * @this {PolygonDrawingTool}
  */
	public undo() {
		// remove a point, and then treat the last one as a temporary one
		this.removeLastPoint();
		var lastInput = this.diagram.lastInput;
		if (lastInput.event instanceof MouseEvent) this.moveLastPoint(lastInput.documentPoint);
	};


	// Public properties

  /**
  * Gets or sets whether this tools draws a filled polygon or an unfilled open polyline.
  * The default value is true.
  * @name PolygonDrawingTool#isPolygon
  * @function.
  * @return {boolean}
  */
	get isPolygon(): boolean { return this._isPolygon; }
	set isPolygon(val: boolean) { this._isPolygon = val; }


  /**
  * Gets or sets whether this tools draws shapes with quadratic bezier curves for each segment, or just straight lines.
  * The default value is false -- only use straight lines.
  * @name PolygonDrawingTool#hasArcs
  * @function.
  * @return {boolean}
  */
	get hasArcs(): boolean { return this._hasArcs; }
	set hasArcs(val: boolean) { this._hasArcs = val; }

  /**
  * Gets or sets the Shape that is used to hold the line as it is being drawn.
  * The default value is a simple Shape drawing an unfilled open thin black line.
  * @name PolygonDrawingTool#temporaryShape
  * @function.
  * @return {Shape}
  */
	get temporaryShape(): go.Shape { return this._temporaryShape }
	set temporaryShape(val: go.Shape) {
		if (this._temporaryShape !== val && val !== null) {
			val.name = "SHAPE";
			var panel = this._temporaryShape.panel;
			panel.remove(this._temporaryShape);
			this._temporaryShape = val;
			panel.add(this._temporaryShape);
		}
	}

  /**
  * Gets or sets the node data object that is copied and added to the model
  * when the drawing operation completes.
  * @name PolygonDrawingTool#archetypePartData
  * @function.
  * @return {Object}
  */
	get archetypePartData(): Object { return this._archetypePartData }
	set archetypePartData(val: Object) { this._archetypePartData = val }
}

